unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons,
  Menus, EditBtn, ExtCtrls, ComboEx, fgl,
  LCLProc;

type
    TMapComments = specialize TFPGMap<string, string>;
    TMapStrings = specialize TFPGMap<string, TStrings>;
  { TForm1 }

  TForm1 = class(TForm)
    btnSave: TButton;
    btnClose: TButton;
    CheckComboBox1: TCheckComboBox;
    CheckGroup1: TCheckGroup;
    ComboBox1: TComboBox;
    tagGroupBox: TCheckGroup;
    tagGroupButton: TSpeedButton;
    tagComboBox: TComboBox;
    Panel1: TPanel;
    ComboBoxEx1: TComboBoxEx;
    EditButton1: TEditButton;
    FlowPanel1: TFlowPanel;
    tagCombo: TComboBox;
    Label1: TLabel;
    Label2: TLabel;
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    tagPopMenu: TPopupMenu;
    tagLabel : TLabel;
    descMemo: TMemo;
    procedure btnSaveClick(Sender: TObject);
    procedure btnCloseClick(Sender: TObject);
    procedure ComboBox1KeyUp(Sender: TObject; var Key: Word; Shift: TShiftState
        );
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormDestroy(Sender: TObject);
    procedure MenuItem1Click(Sender: TObject);
    procedure MenuItem2Click(Sender: TObject);
    procedure tagComboSelect(Sender: TObject);
    procedure tagComboKeyUp(Sender: TObject; var Key: Word;
    Shift: TShiftState);
    procedure FormCreate(Sender: TObject);
    procedure Label1Click(Sender: TObject);
    procedure tagLabelClick(Sender: TObject);
    procedure tagGroupButtonClick(Sender: TObject);
    procedure tagComboBoxKeyUp(Sender: TObject; var Key: Word;
        Shift: TShiftState);
  private
    srcFilePath: string;
    descFilePath: string;
    tagsFilePath: string;
    commentChanged: boolean;        // true => comment is changed
    taglistChanged: boolean;        // true => taglist is changed
    tagGroupOpened: Boolean;        // true => tagGroupBox opened
    tagCheckbox: TCheckBox;
    mapComments : TMapComments;     // [file]=comment
    mapTags : TMapComments;         // [id]=tag
    mapFileTags: TMapStrings;       // [file]=tag(s)
    procedure ShowTagLabel();
    procedure LoadComments();
    function GetComment() : string;
    function EditComment(comment: string) : boolean;
    function SaveComment() : boolean;   
    function LoadTaglist() : boolean;
    function SaveTaglist() : boolean;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
    if (ParamCount > 0) then
    begin
       // todo: verify file exist
       srcFilePath := ParamStr(1);   // source file path
       if (ParamCount > 1) then
          // verify file exist
          descFilePath := ParamStr(2);   // comment file path
          tagsFilePath := ExtractFilePath(descFilePath) + '_tag.list';
    end;
    debugln('FormCreate');
    Label2.Caption := srcFilePath;
    mapComments :=  TMapComments.Create;
    mapTags := TMapComments.Create;
    mapFileTags := TMapStrings.Create;
    // show comments
    LoadComments();
    descMemo.Text := GetComment();
    // show available tag combo
    tagCombo := TComboBox.Create(Self);
    tagCombo.OnSelect:= @tagComboSelect;
    tagCombo.OnKeyUp:= @tagComboKeyUp;
    tagCombo.Tag:= -1;
    tagCombo.Text := 'choose tags';
    //tagCombo.AutoComplete:= true;
    //flowpanel1.InsertControl(tagCombo);
    tagCombo.Parent := flowpanel1;
    LoadTaglist();
    // show assigned tag(s)
    ShowTagLabel();
    tagCombo.Show;
    
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
    mapComments.Free;
end;

procedure TForm1.Label1Click(Sender: TObject);
begin
    ShowMessage('Label click');
end;

procedure TForm1.tagGroupButtonClick(Sender: TObject);
begin
    if (tagGroupOpened = false) then begin
       tagGroupBox.Visible:= true;
       tagGroupbox.Height:= 150;
       self.Height := self.Height+150;
       tagGroupOpened := true;
       // show checkbox for tags
   end
   else begin
       tagGroupbox.Visible:= false;
       self.Height := self.Height-150;
       tagGroupOpened := false;
   end;
end;
// parse the text in comma delimiter: a,b,c
procedure TForm1.tagComboBoxKeyUp(Sender: TObject; var Key: Word;
    Shift: TShiftState);
var
    i: integer;
    tagId: integer;
    tagText: string;
    tags: TStringList;
begin
    if (key <> 13) then exit;
    // enter pressed => split text by ','
    tagText := tagCombobox.Text;
    tags := TStringList.Create;
    tags.AddStrings(tagText.Split([',']));
    for i:=0 to tags.Count-1 do begin
        tagText := tags[i];
        // if tags[n] not in mapTags => add the tag to group and map
        // checked the checkbox
        if (mapTags.IndexOfData(tagText) = -1) then begin
            // create control and add to group
            tagCheckbox := TCheckBox.Create(self);
            with tagCheckbox do begin
                Text := tagText;
                Parent := tagGroupbox;
            end;
            //tagGroupBox.AddControl()
            // add to map
            tagId := tagText.GetHashCode;
            Debugln('  tagId: ', tagId.ToString);
            mapTags.Add(tagId.ToString,tagText);
            taglistChanged := true;
        end;
    end;
end;

// validate user input, then add tag to comboBox
procedure TForm1.tagComboKeyUp(Sender: TObject; var Key: Word;
    Shift: TShiftState);
var
    tagText: string;
    tagId: integer;
begin
    if (key <> 13) then exit;
    // enter pressed => add new tag
    tagText := tagCombo.Text;
    Debugln('Add new tag: ', tagText);
    if (tagCombo.Items.IndexOf(tagText) = -1) then
    begin
        // add to combo and map
        tagCombo.AddItem(tagText, nil);
        tagId := tagText.GetHashCode;
        Debugln('  tagId: ', tagId.ToString);
        mapTags.Add(tagId.ToString,tagText);
        taglistChanged := true;
    end;
end;

procedure TForm1.tagComboSelect(Sender: TObject);
var
    tagCount : Integer;
    tagName: string;
    i: integer;
    key, id: string;
    tagIds: TStrings;//TStringArray;
    idLength: integer;
begin
    Debugln('count=',flowpanel1.ControlCount.ToString);
    tagName := tagCombo.Items[tagCombo.ItemIndex];
    for i:=0 to flowpanel1.ControlCount-1 do
        if (flowpanel1.Controls[i].tag = tagCombo.ItemIndex) then
            exit;
    Debugln('Add ', tagName);
    // add tagName to mapFileTags
    key := ExtractFileName(srcFilePath);
    id := mapTags.Keys[tagCombo.ItemIndex];
    mapFileTags.TryGetData(key, tagIds);
    if (tagIds = nil) then
        tagIds := TStringList.Create;
    // append id to the tagIds array
    {idLength := length(tagIds);
    setlength(tagIds, idLength+1);
    tagIds[idLength] := id;}
    tagIds.Add(id);
    Debugln('  id: ', id);
    mapFileTags[key] := tagIds;
    commentChanged := true;
    
    // create tag label
    tagCombo.Hide;
    tagLabel := TLabel.Create(Self);
    tagLabel.Parent := flowpanel1;
    //tagLabel.Top := tagComboBox.Top;
    //tagLabel.Left := tagComboBox.Left;
    //tagLabel.Height:= tagComboBox.Height;
    //tagLabel.Width:= 100;
    tagLabel.Show;
    tagLabel.Caption:= tagCombo.Items[tagCombo.ItemIndex];
    tagLabel.Tag:= tagCombo.ItemIndex;
    tagLabel.Font.Color:= clHighlight;
    tagLabel.Color:= clSkyBlue;
    tagLabel.OnClick:= @tagLabelClick;

    tagCount := flowpanel1.ControlList.Count - 1;
    flowpanel1.SetControlIndex(tagCombo, tagCount);
    tagCombo.Show;

end;

procedure TForm1.btnSaveClick(Sender: TObject);
begin
    EditComment(descMemo.Text);
    //if (commentChanged) then
        SaveComment();
    if (taglistChanged) then
        SaveTaglist();
end;

procedure TForm1.btnCloseClick(Sender: TObject);
begin
    Self.Close;
end;

procedure TForm1.ComboBox1KeyUp(Sender: TObject; var Key: Word;
    Shift: TShiftState);
begin

end;


procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin

end;
// tagLabel(click)->popup menu
procedure TForm1.tagLabelClick(Sender: TObject);
var
    x : integer;
    y : integer;
begin
    // popup menu for tags
    x := TControl(Sender).ControlOrigin.x;// + self.Left;
//    y := TControl(Sender).Top + 50 + self.Top;
    y := TControl(Sender).ControlOrigin.y + 40;
    tagPopMenu.Tag := TControl(Sender).Tag;
    tagPopmenu.Items[0].Tag:=tagPopMenu.Tag;
    tagPopmenu.Items[1].Tag:=tagPopMenu.Tag;
    Debugln(tagPopMenu.Tag.ToString);
    tagPopMenu.PopUp(x, y);
end;
// popup menu -> Remove.click
procedure TForm1.MenuItem1Click(Sender: TObject);
var
    tagIndex: integer;
    tagName: string;
    i: integer;
    ctrl: TControl;
    key, id: string;
    tagIds: TStrings;//TStringArray;
begin
    tagIndex := TControl(Sender).Tag;
    tagName := tagCombo.Items[tagIndex];
    Debugln(tagName, '.remove');
    for i:=0 to flowpanel1.ControlCount-1 do
        if (flowpanel1.Controls[i].tag = tagIndex) then
        begin
            ctrl := flowpanel1.Controls[i];
            flowpanel1.RemoveControl(ctrl);
            //ctrl.Free;
            
            // remove tagName from mapFileTags
            key := ExtractFileName(srcFilePath);
            id := mapTags.Keys[tagIndex];
            mapFileTags.TryGetData(key, tagIds);
            // remove id from the tagIds
            tagIds.Delete(tagIds.IndexOf(id));
            Debugln('  id: ', id, '@', tagIds.IndexOf(id).ToString);
            mapFileTags[key] := tagIds;
            commentChanged := true;
            Exit;
        end;
end;
// popup menu -> ShowAll
procedure TForm1.MenuItem2Click(Sender: TObject);
var
    tagIndex: integer;
    tagName: string;
begin
    tagIndex := TControl(Sender).Tag;
    tagName := tagCombo.Items[tagIndex];
    Debugln(tagName, '.show');
end;
// Show tag labels from mapFileTags
procedure TForm1.ShowTagLabel();
var
    tagCount : Integer;
    key, id, tagstr: string;
    value: TStrings;
    index: integer;
    item: string;
begin
    key := ExtractFileName(srcFilePath);
    if (mapFileTags.TryGetData(key, value) = false) then Exit;
    for id in value do
    begin
        Debugln('id: ', id);
        if (mapTags.TryGetData(id, tagstr) = false) then Continue;
        index := mapTags.IndexOf(id);
        Debugln('tagstr: ', tagstr);
        // create tag label
        tagLabel := TLabel.Create(Self);
        tagLabel.Parent := flowpanel1;
        tagLabel.Show;
        tagLabel.Caption:= tagstr;
        tagLabel.Tag:= index;
        tagLabel.Font.Color:= clHighlight;
        tagLabel.Color:= clSkyBlue;
        tagLabel.OnClick:= @tagLabelClick;
        // tick the checkbox if tag in tagList
        tagCheckbox := TCheckBox(tagGroupbox.FindChildControl(tagstr));
        //tagCheckbox := tagGroupbox.cont
        if (tagCheckbox <> nil) then begin
            tagCheckbox.Checked:= true;
        end;
    end;
    
    tagCount := flowpanel1.ControlList.Count - 1;
    flowpanel1.SetControlIndex(tagCombo, tagCount);
    tagCombo.Show;

end;

// Read file name and its comments from descrip.ion
// and save into key/value map
procedure TForm1.LoadComments();
var
  i,j: integer;
  idEndPos: integer;
  s: string;
  id, key, value: string;
  keyEndIndex: integer;
  strList: TStringList;
  delim: TSysCharSet;
  ids: TStringList;
begin
    strList := TStringList.Create;
    try
      //Debugln(descFilePath);
      try
          strList.LoadFromFile(descFilePath);
      except
          on EFOpenError do
          begin
             // file not found => create
             Debugln('Comment file not found');
             commentChanged := true;
          end;
      end;

      //Debugln('', strList.Text);
      for i:=0 to strList.Count-1 do begin
          s := strList[i];
          //Debugln(s);
          if (pos('"', s) <> 0) then begin
              s := copy(s, 2, length(s)-1);   // Copy index start from 1
              keyEndIndex := pos('"', s);
              key := copy(s, 1, keyEndIndex-1);
              value := copy(s,keyEndIndex+2, length(s));
          end else begin
              keyEndIndex := pos(' ',s);
              key := copy(s, 1, keyEndIndex-1);
              value := copy(s,keyEndIndex+1, length(s));
          end;
          //Debugln('**', key);
          Debugln('Load:', value);
          // parse tag id
          idEndPos := 0;
          if value.StartsWith('#') = true then
          begin
              {*delim := ['#',' '];
              j := 1;
              ExtractSubstr(value, j, delim);*}
              idEndPos := pos(' ',value);
              id := value.Substring(1,idEndPos-2);
              Debugln(value);
              //value := value.Trim(['#',' ']);
              ids := TStringList.Create;
              //ids.Delimiter:=',';
              //ids.DelimitedText:=id;
              ids.AddStrings(id.Split([',']));
              mapFileTags.Add(key, ids);
              //ids.Free;
              Debugln('  ids: ', id);
          end;

          value := value.Substring(idEndPos,length(value));
          mapComments.Add(key, value);
        end;

    finally
      strList.Free;
    end;
end;
// Get comment of the given file path
function TForm1.GetComment() : string;
var
    i : integer;
    key, value : string;
begin
    key := ExtractFileName(srcFilePath);
    Debugln('Get:', key);
    if (mapComments.TryGetData(key, value) = true) then begin
    //value := mapComments[key];
    //if (value <> '') then begin
        Debugln('  found: ', value);
        Result := value;
    end else
        Result := '';

end;

function TForm1.EditComment(comment: string) : boolean;
var
    key, value : string;
begin
    key := ExtractFileName(srcFilePath);
    mapComments[key] := comment;
    //Debugln('edit: ', key);
    Result := true;
end;

function TForm1.SaveComment() : boolean;
var
    i: integer;
    s: string;
    key, tagid: string;
    idArray: TStringArray;
    ids: TStrings;
    strList: TStringList;
begin
    strList := TStringList.Create;
    for i:=0 to mapComments.Count-1 do begin
        key := mapComments.keys[i];
        // if tag assigned => append tag id
        tagid := '';
        if (mapFileTags.TryGetData(key, ids) = true) then
        begin
            if (ids.Count <> 0) then
            begin
                ids.Delimiter:= ',';
                tagid := ' #' + ids.DelimitedText;
                //tagid := ' #' + tagid.Join(',', idArray);
            end;
        end;
        // if key has space => embrace with ""
        //if ( AnsiContainsStr(key, ' ') = true) then
        if (key.Contains(' ') = true) then
            key := '"' + key + '"';
        s := key + tagid + ' ' + mapComments.Data[i];
        Debugln('save: ', s);
        strList.Add(s);
    end;
    strList.SaveToFile(descFilePath);
    strList.Free;
end;

function TForm1.LoadTaglist() : boolean;
var
    s: string;
    i: integer;
    idEndIndex: integer;
    id, value: string;
    strList: TStringList;
begin
    strList := TStringList.Create;
    try        
        try
            Debugln('Load tag: ', tagsFilePath);
            strList.LoadFromFile(tagsFilePath);
        except
            on EFOpenError do
            begin
                // file not found => create
                Debugln('Tag list not found');
                taglistChanged := true;
            end;
        end;
        for i:=0 to strList.Count-1 do begin
            s := strList[i];
            Debugln(s);
            idEndIndex := pos(' ',s);
            id := copy(s, 1, idEndIndex-1);
            value := copy(s, idEndIndex+1, length(s));
            tagCombo.Items.Add(value);
            mapTags.Add(id, value);
            Debugln('list.id: ', id);
            Debugln('list.tag: ', value);
            // Add checkbox for tags
            tagCheckbox := TCheckBox.Create(self);
            with tagCheckbox do begin
                Text := value;
                Name := value;
                Parent := tagGroupbox;
            end;
        end;

    finally
        strList.Free;
    end;
end;

function TForm1.SaveTaglist() : boolean;
var
    i: integer;
    s, key: string;
    strList: TStringList;
begin
    if (tagCombo.Items.Count = 0) then Exit;

    strList := TStringList.Create;
    for i:=0 to mapTags.Count-1 do begin
        // if key has space => embrace with ""
        key := mapTags.keys[i];
        //if ( AnsiContainsStr(key, ' ') = true) then
        //    key := '"' + key + '"';
        s := key + ' ' + mapTags.Data[i];
        Debugln('tag.save: ', s);
        strList.Add(s);
    end;
    strList.SaveToFile(tagsFilePath);
    strList.Free;
end;

end.
